﻿
namespace Behemoth.SharePoint.Utilities {

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;

    using Microsoft.SharePoint;

    using Behemoth.SharePoint.Caml;
    using Behemoth.SharePoint.Logging;

    // TODO make these part of a site column feature
    public class ReplicationErrorFields {
        public const string ReplicationStartTime = "ReplicationStartTime";
        public const string ReplicationEndTime = "ReplicationEndTime";
        public const string ReplicationStatus = "ReplicationStatus";
        public const string ErrorSummary = "ReplicationError"; //"Replication Error Summary";
        public const string ErrorDetail = "ReplicationErrorDetail"; //"Replication Error Detail";
        public const string ErrorTime = "ReplicationErrorTime"; //"Replication Error Time";
    }

    public class SPFieldReplicationHelper : LoggingEventConsumerBase {

        #region Logging Code Pattern v2.0

        protected override string LOGGING_PRODUCT { get { return "Behemoth"; } }
        protected override string LOGGING_CATEGORY { get { return "Field Replication"; } }

        #endregion Logging Code Pattern

        /// <summary>
        /// Copies common metadata from a folder to all items within it, except this one
        /// Copies all the fields that are shared between the conent types of the
        /// source and target items, except if their group is _Hidden or they
        /// are listed in the feature properties as an excluded fieldName.
        /// </summary>
        /// <param name="sourceFolder"></param>
        /// <param name="excludedItem"></param>
        /// <param name="excludedFieldNames">A list of fieldName names or internal names</param>
        /// <returns></returns>
        public bool ReplicateSharedFields(SPFolder sourceFolder, SPListItem excludedItem, List<string> excludedFieldNames) {
            if (sourceFolder.Item == null)
                throw new ArgumentException("sourceFolder does not have an Item property, therefore it is not part of a document library, which is not allowed for this method.", "sourceFolder");
            SPListItem sourceItem = sourceFolder.Item;
            if (excludedItem != null && excludedItem.ParentList.ID != sourceItem.ParentList.ID)
                throw new ArgumentException("excludedItem and sourceFolder come from different lists, which is not allowed for this method.", "excludedItem");
            SPQuery query = CamlQueryBuilder.BuildAllItemsInFolderExcept(sourceFolder, excludedItem);
            SPListItemCollection targetItems = sourceItem.ParentList.GetItems(query);
            foreach (SPListItem targetItem in targetItems) {
                CopySharedFields(sourceItem, targetItem, excludedFieldNames);
            }
            return false;
        }

        /// <summary>
        /// Copies all the fields that are shared between the conent types of the
        /// source and target items, except if their group is _Hidden or they
        /// are listed in the feature properties as an excluded fieldName.
        /// </summary>
        /// <param name="sourceItem"></param>
        /// <param name="targetItem"></param>
        /// <param name="excludedFieldNames">A list of fieldName names or internal names</param>
        public bool CopySharedFields(SPListItem sourceItem, SPListItem targetItem, List<string> excludedFieldNames) {
            try {
                Log("Starting CopySharedFields.");
                if (sourceItem.ContentType == null || targetItem.ContentType == null)
                    throw new Exception("Something is very, *very* wrong! These items should both have content types.");
                // Get the content type and fields of the source
                List<Guid> sourceFields = GetFields(sourceItem.ContentType);
                // Get the content type and fields of the target
                List<Guid> targetFields = GetFields(targetItem.ContentType);
                // Reduce to the common fields
                List<Guid> sharedFields = GetIntersectionSet(sourceFields, targetFields);
                // Get all the possible fields that are in the list, create excluded fields list
                SPFieldCollection allFields = sourceItem.ParentList.Fields; //sourceItem.Fields;
                List<Guid> excludedFields = CreateExcludedFieldsList(allFields, excludedFieldNames);
                // Remove any excluded fields
                List<Guid> replicateFields = RemoveItems(sharedFields, excludedFields);

                Log("Final list of fields to replicate is: ");
                List<SPField> testFields = new List<SPField>();
                foreach (Guid id in replicateFields) {
                    SPField field = sourceItem.Fields[id];
                    testFields.Add(field);
                    Log(string.Format(
                        "  field Title='{0}', InternalName='{1}'.", 
                        field.Title, 
                        field.InternalName
                    ), TraceSeverity.Verbose);
                }
                
                // Do the fieldName replication
                CopySharedFields(sourceItem, targetItem, replicateFields);

                Log("Finished CopySharedFields() successfully.");
                return true;
                // Since we are replying on content types, we can be pretty certain that
                // both the source and the target actually have these fields in them
                //
                // It would probably have been more efficient, strictly speaking
                // to exclude the excludedFields and hidden fields earlier, but
                // I thought you know this way it is easier to understand, and
                // I think maybe it will hold up to changes a bit better.
            } catch (Exception ex) {
                Log("Exception in CopySharedFields.", TraceSeverity.Exception);
                LogReplicationErrorToItem(sourceItem, targetItem, ex); // Also logs the exception
            }
            return false;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="sourceItem"></param>
        /// <param name="targetItem"></param>
        /// <param name="fieldsToCopy"></param>
        /// <returns></returns>
        private bool CopySharedFields(SPListItem sourceItem, SPListItem targetItem, List<Guid> fieldsToCopy) {
            if (sourceItem == null || targetItem == null)
                throw new ArgumentNullException("Both sourceItem and targetItem ,ust be valid instances of SPListItem objects.");
            if (sourceItem.ParentList.ID != targetItem.ParentList.ID)
                throw new ArgumentException("sourceItem and targetItem come from different lists, which is not allowed for this method.", "sourceItem,targetItem");
            Log("Initiating CopySharedFields().");
            Log(string.Format(
                "Replicating fields from sourceItem.ID={0} to targetItem.ID={1} on List at {2}/{3}.",
                sourceItem.ID,
                targetItem.ID,
                sourceItem.ParentList.ParentWeb.Url,
                sourceItem.ParentList.Title
            ));
            DateTime startTime = DateTime.Now;
            LogReplicationEventToItem(targetItem, ReplicationStatus.InProgress, startTime, DateTime.MinValue);
            targetItem.UpdateWithCheckOutAndIn();
            foreach (Guid id in fieldsToCopy) {
                if (sourceItem[id] != null && !string.IsNullOrEmpty(sourceItem[id].ToString())) {
                    targetItem[id] = sourceItem[id];
                    // these are the same fieldName, the types should match!
                }
            }
            LogReplicationEventToItem(targetItem, ReplicationStatus.Completed, startTime, DateTime.Now);
            targetItem.UpdateWithCheckOutAndIn();
            Log("Finishing CopySharedFields() successfully.");
            return true;
        }

        #region Excluded Field Helper Functions

        /// <summary>
        /// Not efficient, but better than being wrong...
        /// </summary>
        /// <param name="source"></param>
        /// <returns></returns>
        private List<string> EnsureToUpperAndSystemFields(List<string> source) {
            Log("Starting EnsureToUpperAndSystemFields().");
            List<string> dest = new List<string>();
            if (source != null) {
                foreach (string item in source) {
                    dest.Add(item.ToUpper());
                }
            }
            // always exclude these important system fields
            if (!dest.Contains("TITLE"))
                dest.Add("TITLE");
            if (!dest.Contains("FILELEAFREF"))
                dest.Add("FILELEAFREF");
            // TODO add more here
            Log("Finishing EnsureToUpperAndSystemFields().");
            return dest;
        }

        /// <summary>
        /// Creates a list of fields that shouldn't be replicated.
        /// </summary>
        /// <param name="allFields">All the fields in a list</param>
        /// <param name="excludedFieldNames">A list of fieldName names or internal names. Assumed to be in uppercase character</param>
        /// <returns></returns>
        public List<Guid> CreateExcludedFieldsList(SPFieldCollection allFields) {
            return CreateExcludedFieldsList(allFields, null);
        }
        public List<Guid> CreateExcludedFieldsList(SPFieldCollection allFields, List<string> excludedFieldNames) {
            List<Guid> excludedFields = new List<Guid>();
            Log("Starting CreateExcludedFieldsList().");
            excludedFieldNames = EnsureToUpperAndSystemFields(excludedFieldNames);
            foreach (SPField field in allFields) {
                // Exclude any fieldName that is in _Hidden group, plus the Title
                // fieldName. Also exclude any fieldName that matches the names in the
                // excluded fields under Properties. ToUpper is our "poor man's"
                // case insensitive matching here
                if (!excludedFields.Contains(field.Id)
                  && (field.Group == "_Hidden"
                  || excludedFieldNames.Contains(field.Title.ToUpper())
                  || excludedFieldNames.Contains(field.InternalName.ToUpper())
                )) {
                    excludedFields.Add(field.Id);
                }
                // You know at the end of the day I am wondering if this is really more efficient
                // than the way I did it before where I searched for each excluded fieldName using
                // TryGetField and then added their Guids to the excluded fields list... I wonder...
            }
            Log("Finishing CreateExcludedFieldsList().");
            return excludedFields;
        }

        #endregion

        #region Field List Manipulation Helpers

        private static List<Guid> GetFields(SPContentType ct) {
            List<Guid> fieldList = new List<Guid>();
            foreach (SPField field in ct.Fields) {
                fieldList.Add(field.Id);
            }
            return fieldList;
        }
        private static List<Guid> GetIntersectionSet(List<Guid> list1, List<Guid> list2) {
            // could have used AddRange() and Remove(), pretty sure that would actually be less efficient though
            List<Guid> intersectList = new List<Guid>();
            foreach (Guid id in list1) {
                if (list2.Contains(id))
                    intersectList.Add(id);
            }
            return intersectList;
        }
        private static List<Guid> RemoveItems(List<Guid> list, List<Guid> listToRemove) {
            List<Guid> newList = new List<Guid>();
            newList.AddRange(list); // make a copy, don't affect the source list
            foreach (Guid id in listToRemove) {
                if (newList.Contains(id))
                    newList.Remove(id);
            }
            return newList;
        }

        #endregion

        #region Logging to List Item Helpers

        public bool LogReplicationErrorToItem(SPListItem sourceItem, SPListItem targetItem, Exception ex) {
            // TODO Log this error immediately
            string msg = string.Format("Error copying from ID={0} to ID={1}.", sourceItem.ID, targetItem.ID);
            string detail = string.Format("The error was '{0}'. Stack trace: {1}", ex.Message, ex.StackTrace);
            Log(msg, TraceSeverity.Exception); Log(ex);
            bool success = LogReplicationErrorToItem(targetItem, msg, detail);
            // in a foreach loop it may not make much sense to log the error in the source,
            // but it might fail in the destination, so it is better than nothing
            if (!success)
                success = LogReplicationErrorToItem(sourceItem, msg, detail);
            return success;
        }
        public bool LogReplicationErrorToItem(SPListItem item, string errMsg, string details) {
            try {
                //item.ParentList
                string statusText = StringEnum<ReplicationStatus>.Value(ReplicationStatus.Error);
                item[item.Fields.GetFieldId(ReplicationErrorFields.ReplicationStatus)] = statusText;
                item[item.Fields.GetFieldId(ReplicationErrorFields.ErrorSummary)] = errMsg;
                item[item.Fields.GetFieldId(ReplicationErrorFields.ErrorDetail)] = details;
                item[item.Fields.GetFieldId(ReplicationErrorFields.ErrorTime)] = DateTime.Now;
                item.UpdateWithCheckOutAndIn();
                return true;
            } catch (Exception ex) {
                Log(ex);
            }
            return false;
        }

        public bool LogReplicationEventToItem(SPListItem item, ReplicationStatus status, DateTime startTime, DateTime endTime) {
            try {
                //ReplicationStatus status = (ReplicationStatus)Enum.Parse(typeof(ReplicationStatus), statusValue.ToString(), true);
                string statusText = StringEnum<ReplicationStatus>.Value(status);
                item[item.Fields.GetFieldId(ReplicationErrorFields.ReplicationStatus)] = statusText;
                if (startTime == DateTime.MinValue || startTime == DateTime.MaxValue)
                    item[item.Fields.GetFieldId(ReplicationErrorFields.ReplicationStartTime)] = null;
                else
                    item[item.Fields.GetFieldId(ReplicationErrorFields.ReplicationStartTime)] = startTime;
                if (endTime == DateTime.MinValue || endTime == DateTime.MaxValue)
                    item[item.Fields.GetFieldId(ReplicationErrorFields.ReplicationEndTime)] = null;
                else
                    item[item.Fields.GetFieldId(ReplicationErrorFields.ReplicationEndTime)] = endTime;
                return true;
            } catch (Exception ex) {
                Log(ex);
                LogReplicationErrorToItem(item, ex.Message, ex.StackTrace);
            }
            return false;
        }

        #endregion

    }

    public enum ReplicationStatus {
        [EnumStringValue("Idle")]
        Idle,
        [EnumStringValue("In Progress")]
        InProgress,
        [EnumStringValue("Completed")]
        Completed,
        [EnumStringValue("Error")]
        Error
    }

} // namespace
